package aim;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Vector;

/**
 * InputParser.java
 * 
 * Main algorithm class. Parses all input files, pulling word tokens from the
 * input based on break characters. Also strips out stop words and processes
 * remaining words. Processed words are written to a BitManager and saved to
 * disk.
 * 
 * Modified and reused from Project 2 Team 1 (WMI) code
 * @author Andrew Bernard, Zachary M. Allen
 */
public class AIMIndex {	
	
	/** Keeps track of the current line number */
	private int currentLine = 0;

	/** Vector of InputFile objects */
	private Vector inputs = null;

	/** Hash map to determine if words are stop words */
	private HashMap stopWordMap = null;

	/** Hash map to determine if chars are break chars */
	private HashMap varBreakChars = null;
	
	/** File object representing stop word file */
	private File stopWordFile = null;

	public AIMIndex(Options options) {
		this.inputs = options.getInputs();
		this.stopWordFile = options.getStopWordFile();
		this.currentLine = 0;

		/* Set break characters */
		varBreakChars = new HashMap();
    char[] breakChars = options.getBreakChars();
		for (int i = 0; i < breakChars.length; i++) {
			varBreakChars.put(new Character(breakChars[i]), new Object());
		}

		if (stopWordFile != null) {
			parseStopFile(stopWordFile);
		}
	}

	/**
	 * This is the main algorithm method. It parses the input and creates the
	 * index file.
	 * 
	 * @throws    IOException    Thrown if an error reading from input occurs
	 */
	public void write() throws IOException {		
		int nextChar = 0;
		BitManager bm = new BitManager();
		
		/* MAIN HEADER INFORMATION */		
		/* Add date and time of creation */
		GregorianCalendar gc = new GregorianCalendar();
		gc.setTimeInMillis(System.currentTimeMillis());
		bm.addBits((1 + gc.get(Calendar.MONTH)), Constants.MONTH_BITS);
		bm.addBits(gc.get(Calendar.DATE), Constants.DATE_BITS);
		bm.addBits(gc.get(Calendar.YEAR), Constants.YEAR_BITS);
		bm.addBits(gc.get(Calendar.HOUR), Constants.HOUR_BITS);
		bm.addBits(gc.get(Calendar.MINUTE), Constants.MINUTE_BITS);
		bm.addBits(gc.get(Calendar.SECOND), Constants.SECOND_BITS);
		
		/* Add break characters used in creation */
		for (int j = Constants.FIRST_CUSTOM_BC; j <= Constants.LAST_CUSTOM_BC; j++) {
			if (isBreakCharacter(j)) {
				bm.addBits(1, 1);
			} else {
				bm.addBits(0, 1);
			}
		}
		
		/* Add stop file used, if any */
		if (stopWordFile == null) {
			bm.addBits(0, Constants.FILE_LENGTH_BITS);
		} else {
			bm.addBits(stopWordFile.getAbsolutePath().length(), Constants.FILE_LENGTH_BITS);
			for (int k = 0; k < stopWordFile.getAbsolutePath().length(); k++) {
				bm.addBits(stopWordFile.getAbsolutePath().charAt(k), Constants.BITS_PER_ASCII_BYTE);
			}
		}
		
		/* Add # of input files */
		bm.addBits(inputs.size(), Constants.NUM_INPUT_FILES_BITS);
		
		for (int i = 0; i < inputs.size(); i++) {			
			int wordCount = 0;
			currentLine = 0;
			
			InputFile f = (InputFile) (inputs.elementAt(i));			
			BufferedInputStream fis = new BufferedInputStream(new FileInputStream(f.getFile()));			
			do {
				String word = getNextWord(fis).toLowerCase();
				wordCount++;
				if (!isStopWord(word)) {
					f.processWord(currentLine, word);					
				}
				nextChar = Constants.peek(fis);
			} while (nextChar != Constants.EOF);
			fis.close();
			f.setWordCount(wordCount);
			
			/* Add file name length and file name */
      String filePath = f.getFile().getAbsolutePath();
			bm.addBits(filePath.length(), Constants.FILE_LENGTH_BITS);
			for (int a = 0; a < filePath.length(); a++) {
				bm.addBits(filePath.charAt(a), Constants.BITS_PER_ASCII_BYTE);
			}
			
			/* Add # chars in file */
			bm.addBits(f.getCharCount(), Constants.FILE_CHARS_BITS);
			
			/* Add # words in file */
			bm.addBits(f.getWordCount(), Constants.FILE_WORDS_BITS);
			
			/* Add # lines in file */
			bm.addBits(f.getLineCount(), Constants.FILE_LINES_BITS);
			
			/* Add # of bits per bitmask */
			bm.addBits(Constants.DEFAULT_BITS_PER_BITMASK, 7);
			
			for (int c = 0; c < f.getNumBitmasks(); c++) {				
				/* Add bitmask */
				bm.addBits(f.getBitmask(c), Constants.DEFAULT_BITS_PER_BITMASK);			
			}			
		}		
		bm.save(Constants.OUTPUT_FILE_NAME);		
	}

	/**
	 * Parse the stop words file.
	 * @param    stopFile    Stop word file to parse
	 */
	private void parseStopFile(File stopFile) {
		int nextChar = 0;
		try {
			BufferedInputStream stopFileStream = new BufferedInputStream(new FileInputStream(stopFile));
			stopWordMap = new HashMap();
			do {
				String word = getNextWord(stopFileStream);
				stopWordMap.put(word.toLowerCase(), new Object());
				nextChar = Constants.peek(stopFileStream);
			} while (nextChar != Constants.EOF);
			stopFileStream.close();
		} catch (IOException ex) {
			/* File is checked before it's passed here */
		}
	}

	/**
	 * Returns true if <code>word</code> is a stop word, false otherwise
	 * @param    word    Word to check stop words for
	 * @return    True if <code>word</code> is a stop word, false otherwise
	 */
	private boolean isStopWord(String word) {
		return stopWordMap != null && stopWordMap.get(word) != null;
	}

	/**
	 * This method tests <code>toCheck</code> against both the fixed
	 * and variable sets of break characters.
	 * @param toCheck will be tested to see if it is a break character
	 * @return <code>true</code> if <code>toCheck</code> is a break 
	 *         character, <code>false</code> otherwise
	 * @throws IOException 
	 */
	private boolean isBreakCharacter(int toCheck) throws IOException {
		boolean isBreakChar = false;
		Character key = new Character((char) toCheck);
		isBreakChar = (varBreakChars.get(key) != null);
		isBreakChar = isBreakChar || toCheck < Constants.FIRST_CUSTOM_BC || toCheck > Constants.LAST_CUSTOM_BC;
		return isBreakChar;
	}

	/**
	 * Reads an input stream until a non-break character or EOF is encountered.
	 * @param    fis    Input stream to read
	 * @throws    IOException
	 */
	private void clearBreakCharacters(BufferedInputStream fis) throws IOException {
		int nextChar = 0;
		do {
			nextChar = Constants.peek(fis);
			if (isBreakCharacter(nextChar)) {
				fis.skip(1);
			}
			if (nextChar == Constants.CR) {
				int temp = Constants.peek(fis);
				if (temp == Constants.LF) {
					fis.skip(1);
				}
			}
			if (nextChar == Constants.CR || nextChar == Constants.LF) {
				currentLine++;
			}
		} while (nextChar != Constants.EOF && isBreakCharacter(nextChar));
	}

	/**
	 * Gets the next word from an input stream.
	 * @param    fis    Input stream to read from
	 * @return    Next word in input stream
	 * @throws    IOException
	 */
	private String getNextWord(BufferedInputStream fis) throws IOException {
		int nextChar = 0;
		String word = "";
		clearBreakCharacters(fis);
		do {
			word += (char) fis.read();
			nextChar = Constants.peek(fis);
		} while (nextChar != Constants.EOF && !isBreakCharacter(nextChar));
		return word;
	}
	
}